---
title: 事务
description: 事务
sidebar_position: 13
---
## 定义
事务（traction）是进行一次处理的基本单元，要么完全执行，要么都不执行。
事务保证了一次处理的完整性，也保证了数据库中数据的一致性。它是一种高级的数据处理方式，如果在增删改查的时候任一环节出现错误，它允许我们回滚到之前的状态。

因此，事务非常适合应用在安全性高的场景里，比如金融行业等。

## 事务的特性ACID
事务要么全部执行，要么都不执行，具有四个特性：
1. A， 原子性（Atomicity）。原子的概念就是不可分割，可以把它理解成组成物质的基本单位，也就是我们进行数据处理操作的基本单位。
2. C,  一致性（Consistency)。一致性指的就是数据库在进行事务操作后，会由原来的一致状态，变成另一种一致的状态。也就是说当事务提交之后，或者当事务发生回滚后，数据库的完整性约束不能被破坏。
3. I， 隔离性（Isolation)。每个事务都是彼此独立的， 不会收到其它事务的执行影响。一个事务再提交前，对其他事务都是不可见的。
4. D， 持久性（Durability)。 事务提交之后都数据的修改是持久性的。即便是系统出故障的情况下，比如系统崩溃或者存储介质发生故障，数据的修改依然是有效的。因为当事务完成，数据库的日志就会被更新，这时可以通过日志，让系统恢复到最后一次成功的更新状态。

ACID 可以说是事务的四大特性，原子性是基础，隔离性是手段，一致性是约束条件，而持久性是目的。
### 一致性
一致性本身是由具体的业务定义的，也就是说，任何写入数据库中的数据都要满足事先定义好的约束规则。

比如，在数据表中我们将姓名字段设置为唯一性约束，这时当事务进行提交或者事务发生回滚的时候，如果数据表中的姓名非唯一，就破坏了事务的一致性要求。所以说，事务操作会让数据表的状态变成另一种一致的状态，如果事务中的某个操作失败了，系统
就会自动撤销当前正在执行的事务，返回到事务操作之前的状态。

### 持久性
持久性是通过事务日志来保证的，事务日志包括了回滚日志和重做日志。当我们通过事务对数据进行修改的时候，首先会将数据库的变化信息记录到重做日志中，然后再对数据库中对应的行进行修改。这样做的好处是，即使数据库系统崩溃，数据库也能找到没有更新到数据库中的重做日志，重新执行，从而使事务具有持久性。

## 事务的控制
Oracle 是支持事务的，而在 MySQL 中，则需要选择适合的存储引擎才可以支持事务，可以通过 `SHOW ENGINES` 命令来查看当前 MySQL 支持ide存储引擎有哪些，以及这些存储引擎是否支持事务。
```sql
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.08 sec)

```
在 MySQL 中，InnoDB 是支持事务的，而 MyISAM 存储引擎不支持事务。

事务的常用控制语句都有下面这些：
1. START TRACTION 或者 BEGIN;，作用是显式开启一个事务。
2. COMMIT ：提交事务，当事务提交后，对数据库的修改是持久的。
3. ROLLBACK 或者 ROLLBACK TO `[SAVEPOINT]`, 回滚事务，撤销正在执行的所有没有提交的修改，或者将事务回滚到某个保存点。
4. SAVEPOINT: 在事务中创建保存点，方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
5. RELEASE SAVEPOINT: 释放保存点，删除保存点。
6. SET TRANSACTION , 设置事务的隔离界别。

使用事务有两种方式，分别为显示事务和隐式事务。隐式事务实际上就是自动提交，Oracle 默认不自动提交，需要手写 COMMIT 命令，而 MySQL 默认自动提交，当前也可以配置 MySQL 的参数：
```sql
mysql > set autocommit = 0; // 关闭自动提交
mysql > set autocommit = 1; // 开启自动提交
```
在 MySQL 的默认状态下，下面这个事务最后的处理结果是什么：
```sql
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO test SELECT '关羽';
COMMIT;
BEGIN;
INSERT INTO test SELECT '张飞';
INSERT INTO test SELECT '张飞';
ROLLBACK;
SELECT * FROM test;
```
结果是：
```sql
mysql> SELECT * FROM test;
+-------+
| name  |
+-------+
| 关羽  |
+-------+
1 row in set (0.00 sec)
```
在这个事务中，整个 SQL 一共执行了 2 个事务，第一个是插入 “关羽”，提交后执行成功，第二个是插入两次 “张飞”，这里需要注意的是，将 name 设置为了主键，也就是说主键的值是唯一的，那么第二次插入“张飞“ 时就会产生错误，然后执行 ROLLBACK 相当于对事务进行了回滚，所以
最终结果只有一行数据，也就是第一个事务执行之后的结果，即”关羽“。

如果进行下面的操作又会怎么样呢？
```sql
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO test SELECT '关羽';
COMMIT;
INSERT INTO test SELECT '张飞';
INSERT INTO test SELECT '张飞';
ROLLBACK;
SELECT * FROM test;
```


上次操作把两次插入 "张飞" 放到一个事务中，而这次操作它们不在同一个事务里，那么对于 MySQL 来说，默认情况下这实际就是两个事务，因为在 autocommit = 1 的情况下，MySQL 会进行隐式事务，也就是自动提交，因此在第一次插入 ”张飞“ 之后，数据表里就存在了两行数据，第二次插入 ”张飞“ 就会报错。
`1062 - Duplicate entry '张飞' for key 'PRIMARY'`

最后在执行 ROLLBACK 的时候，实际上事务已经自动提交了，就没法进行回滚了。

```sql
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
SET @@completion_type = 1;
BEGIN;
INSERT INTO test SELECT '关羽';
COMMIT;
INSERT INTO test SELECT '张飞';
INSERT INTO test SELECT '张飞';
ROLLBACK;
SELECT * FROM test;
```
上面这个操作，在 MySQL 中，会自动将两个 SQL 放到一个事务中，那么结果就会和上面一样，只有一行数据，即第一个事务执行之后的结果。
1. completion_type = 0 , 默认值，执行 COMMIT 的时候会提交事务，在执行下一个事务时，还需要使用 START TRANSACTION 或者 BEGIN 显式开启事务。
2. completion_type = 1 , 当我们提交事务后，相当于执行了 COMMIT AND CHAIN ，也就是开启一个链式事务，即当我们提交事务之后会开启一个相同隔离级别的事务。
3. completion_type = 2 , 这种情况下 COMMIT = COMMIT AND RELEASE ,也就是当我们提交后，会自动与服务器断开连接。

在上面的这段代码里，使用了 completion = 1 ,也就是说当提交之后，相当于在下一行写了一个 START TRANSACTION 或 BEGIN 。 此时两次插入 ”张飞“ 会被认为是在同一个事务之内的操作，
那么第二次插入”张飞“就会导致事务失败，而回滚也将这次事务进行了撤销，所以最终结果只有一行数据，即第一个事务执行之后结果。

当我们设置 autocommit = 0 时， 不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务，都需要用 COMMIT 进行提交，让事务生效，使用 ROLLBACK 对事务进行回滚。

当我们设置 autocommit = 1 ，每条 SQL 语句都会自动进行提交。

不过这时，如果采用 START TRANSACTION 或者 BEGIN 的方式来显式的开启事务，那么这个事务只有在 COMMIT 时才会生效，在 ROLLBACK 时才会回滚。

## 事务隔离
隔离性是事务的基本特征之一，它可以防止数据库在并发处理时出现数据不一致的情况。最严格的情况下，可以采用串行化的方式来执行每一个事务，这就意味着事物之间是相互独立的，不存在并发的情况。然而在实际生产环境下，考虑到随着用户量的增多，会存在大规模并发访问的情况，这就要求
数据库有更高的吞吐能力，这时候串行化的方式就无法满足数据库高并发访问的需求，还需要降低数据库的隔离标准，来换取事务之间的并发能力。

有时候需要牺牲一定的正确性来换取效率的提升，也即是说，需要通过设置不同的隔离等级，以便在正确性和效率之间进行平衡。同时，随着 RDBMS 种类和应用场景的增多，数据库的设计者需要统一
对数据库隔离级别进行定义，说明这些隔离标准都解决了那些问题。
### 事务并发处理可能存在的异常

SQL-92 标准中已经对 3 中异常情况进行了定义，这些异常情况级别分别为脏读（Dirty Read）、不可重复读（Nonrepeatable Read）和幻读（Phantom Read）。

脏读、不可重复读和幻读 都代表了什么，用一个例子来举例。比如说我们有个英雄表 heros_temp ,如下所示：


![](https://wkq-img.oss-cn-chengdu.aliyuncs.com/site/66cbeb736b5e92dc271f86b7152d46dd.png)

这张英雄表，我们会记录很多英雄的姓名，假设我们不对事务进行隔离操作，那么数据库在进行事务的并发处理时会出现什么样的情况。
#### 脏读
第一天，小张访问数据库，正在进行事务操作，往里面写入一个新的英雄 ”吕布“：
```sql
SQL > BEGIN ;
SQL > INSERT INTO heros_temp VALUES(4, '吕布');
```
当小张还没有提交事务的时候，小李又对数据表进行了访问，他想看下这张表都有那些英雄：
```sql
SQL > SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 刘备  |
|  2 | 张飞  |
|  3 | 关羽  |
|  4 | 吕布  |
+----+-------+
4 rows in set (0.00 sec)
```

此时小张还没有提交事务，但是小李却读到了小张还没有提交的数据，这种现象我们称之为脏读。
#### 不可重复读
第二天，小张想查看 id = 1 的英雄是谁，于是他进行了 SQL 查询：
```sql
SQL> BEGIN;
SQL> SELECT name FROM heros_temp WHERE id = 1;
+-------+
| name  |
+-------+
| 刘备  |
+-------+
1 row in set (0.00 sec)
```
然而此时，小李开始了一个事务操作，他对 id = 1 的英雄姓名进行了修改，把原来的 "张飞" 改成了 "张翼德" :
```sql
SQL > BEGIN;
SQL > UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
```
然后小张再次进行查询，同样也是查看 id = 1 的英雄是谁：
```sql
SQL > SELECT name FROM heros_temp WHERE id = 1;
+-------+
| name  |
+-------+
| 张翼德 |
+-------+
1 row in set (0.00 sec)
```
两次查询的结果并不一样，这种情况称之为，不可重复读。也就是同一条记录，两次读取的结果不同。
![](https://wkq-img.oss-cn-chengdu.aliyuncs.com/site/20250921232117.png)

#### 幻读
第三天，小张想要看下数据表里都有那些英雄，他开始执行下面这条语句：
```sql
SQL > SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 刘备  |
|  2 | 张飞  |
|  3 | 关羽  |
+----+-------+
4 rows in set (0.00 sec)
```
这是当小张执行完之后，小李又开始了一个事务，往数据库里插入一个新的英雄 ”吕布“：
```sql
SQL > BEGIN;
SQL > INSERT INTO heros_temp VALUES(4, '吕布');
```

不巧的是，小张这时忘记了英雄都有那些，又重新执行了一遍查询；
```sql
SQL > SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 刘备  |
|  2 | 张飞  |
|  3 | 关羽  |
|  4 | 吕布  |
+----+-------+
5 rows in set (0.00 sec)
```
他发现这一次查询多了一个英雄，原来只有 3 个，现在变成了 4 个。这种异常情况称之为幻读。

1. 脏读： 读到了其它事务还没有提交的数据。
2. 不可重复读：对某数据进行读取，发现两次读取的结果不同，也就是说没有读到相同的内容。这是因为有其它事务对这个数据同时进行了修改或删除。
3. 幻读： 事务 A 根据条件查询得到了 N 条数据，但此时事务 B 更改或者增加了 M 条符合事务 A 查询条件的数据，这样当事务 A 再次进行查询的时候发现会有 N + M 条数据，产生了幻读。

### 事务的隔离级别有哪些
脏读、不可重复读和幻读这三种异常情况，是在 SQL-92 标准中定义的，同时 SQL-92 标准定义了 4 中隔离级别来解决这些异常情况。

解决异常数量从少到多的顺序（比如读未提交可能存在 3 种异常，可串行化不会存在这些异常），决定了隔离级别的高度，这四种隔离级别从低到高分别是：读未提交（READ UNCOMMITED）、读已提交（READ COMMITED）、可重复读（REPEATABLE READ）和可串行化（SERIALIZABLE）。这些隔离级别能解决的异常情况如下表所示：

![](https://wkq-img.oss-cn-chengdu.aliyuncs.com/site/b07103c5f5486aec5e2daf1dacfd6f95.png)
可串行化能避免所有的异常情况，而读未提交则允许异常情况发生。

关于这四种级别，简单来说：
1. 读未提交，也就是允许读到未提交的数据，这种情况下查询是不会使用锁的，可能会产生脏读、不可重复读、幻读等情况。
2. 读已提交就是只能读到已经提交的内容，可以避免脏读的产生，属于 RDBMS 种常见的默认隔离级别（比如 Oracle 和 SQL Server），但如果想要避免不可重复读或者幻读，就需要在 SQL 查询的时候写带加锁的 SQL 语句。
3. 可重复读： 保证一个事务在相同查询条件下两次查询得到的数据结果是一致的，可以避免不可重复读和脏读，但无法避免幻读。MySQL 的默认隔离级别就是可重复读。
4. 可串行化： 将事务进行串行化，也就是在一个队列种按照顺序执行，可串行化是最高级别的隔离等级，可以解决事务读取中所有可能出现的异常情况，但是它牺牲了系统的并发性。

## 模拟三种异常
首先需要一个英雄数据表 heros_temp, 具体表结构和数据如下：
```sql
-- ----------------------------
-- Table structure for heros_temp
-- ----------------------------
DROP TABLE IF EXISTS `heros_temp`;
CREATE TABLE `heros_temp`  (
  `id` int(11) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of heros_temp
-- ----------------------------
INSERT INTO `heros_temp` VALUES (1, '张飞');
INSERT INTO `heros_temp` VALUES (2, '关羽');
INSERT INTO `heros_temp` VALUES (3, '刘备');
```
模拟的时候需要开两个 MySQL 客户端，分别是 客户端 1 和客户端 2.

在客户端 1 中，先来查看当前会话的隔离级别，使用命令：
```sql
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.15 sec)
```
然后能看到当前的隔离级别是 REPEATABLE-READ， 也就是可重复读。

现在把隔离级别降到最低，设置为 READ UNCOMMITTED(读未提交)。
```sql
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.03 sec)
```
然后再查看当前会话（SESSION）下的隔离级别，结果如下：
```sql
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | READ-UNCOMMITTED |
+-----------------------+-----------------+
1 row in set (0.00 sec)
```

因为 MySQL 默认是事务自动提交，还需要将 autocommit 参数设置为 0 ，如下：
```sql
mysql> SET autocommit = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | OFF   |
+---------------+-------+
1 row in set (0.00 sec)
```
接着我们以同样的操作启动客户端 2 ，也就是将隔离级别设置为 READ UNCOMMITTED（读未提交），并设置 autocommit 为 0。





### 脏读
在客户端 2 中开启一个事务，在 heros_temp 表中写入一个新的英雄 “卢布”，注意，这时候不要提交。
```sql
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO heros_temp VALUES (4, '卢布');
Query OK, 1 row affected (0.00 sec)
```
然后在客户端 1 中查询 heros_temp 表，结果如下：
```sql
mysql> SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 张飞  |
|  2 | 关羽  |
|  3 | 刘备  |
|  4 | 卢布  |
+----+-------+
4 rows in set (0.00 sec)
```
这里出现了一个脏读，客户端 1 中读取了客户端 2 未提交的新英雄“吕布”，实际上客户端 2 可能马上回滚，从而造成了“脏读”。


### 不可重复读
用客户端 1 来查看 id = 1 的英雄：
```sql
mysql> SELECT * FROM heros_temp WHERE id = 1;
+----+-------+
| id | name  |
+----+-------+
|  1 | 张飞  |
+----+-------+
1 row in set (0.00 sec)
```
然后用客户端 2 对 id = 1 的英雄姓名进行修改：
```sql
mysql> UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 ```
用客户端 1 再次查看 id = 1 的英雄：
```sql
mysql> SELECT * FROM heros_temp WHERE id = 1;
+----+-------+
| id | name  |
+----+-------+
|  1 | 张翼德 |
+----+-------+
1 row in set (0.00 sec)
```
这里出现了一个不可重复读，客户端 1 中读取了客户端 2 修改的英雄“张翼德”，实际上客户端 2 可能马上回滚，从而造成了“不可重复读”。

### 幻读
首先用客户端查询数据库中的所有英雄：
```sql
mysql> SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 张飞  |
|  2 | 关羽  |
|  3 | 刘备  |
+----+-------+
3 rows in set (0.00 sec)
```
然后用客户端 2 来添加一个新的英雄 “卢布” ：
```sql
mysql> INSERT INTO heros_temp VALUES (4, '卢布');
Query OK, 1 row affected (0.00 sec)
```
用客户端 1 再次查询数据库中的所有英雄：
```sql
mysql> SELECT * FROM heros_temp;
+----+-------+
| id | name  |
+----+-------+
|  1 | 张飞  |
|  2 | 关羽  |
|  3 | 刘备  |
|  4 | 卢布  |
+----+-------+
4 rows in set (0.00 sec)
```
这里出现了一个幻读，客户端 1 中读取了客户端 2 新增的英雄“卢布”，实际上客户端 2 可能马上回滚，从而造成了“幻读”。
## 总结
在进行数据库操作的时候，可能会失败，但正式因为有事务的存在，即使在数据库操作失败的情况下，也能保证数据的一致性。

同样，多个应用程序访问数据库的时候，事务可以提供隔离，保证事务之间不被干扰。最后，事务一旦提交，结果就会说永久性的，即便是系统崩溃了，数据库也可以对数据进行恢复。

事务是数据库区别于文件系统的重要特征之一，有了事务就会让数据库始终保持一致性，同时我们还能通过事务的机制恢复到某个时间点，这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

隔离级越低，意味着系统吞吐量（并发程度）越大，但同时也意味着出现异常问题的可能性更大。在实际使用过程中，我们往往需要在性能和正确性上进行权衡和取舍，没有完美的解决方案，只有适合与否。

隔离级别越高，就是越接近串行化操作。


